export declare interface DragOptions {
  container: HTMLElement
  direction?: 'horizontal' | 'vertical'
  sticky?: 'horizontal' | 'vertical' | 'top' | 'right' | 'bottom' | 'left' | true,
  draggingClassName?: string
}

export default class Drag {
  constructor(el: HTMLElement, options: Partial<DragOptions>)
  constructor(options: Partial<DragOptions> & { el: HTMLElement })
  constructor(el: any, options?: any) {
    this.init(el, options)
  }

  // 默认配置
  private readonly defaultOptions = {
    container: document.body,
    draggingClassName: 'dragging'
  }

  // 被拖拽元素
  private el: HTMLElement

  // 拖拽配置
  private options: DragOptions

  // el 未发生拖拽前的 z-index
  private defaultZIndex: string

  // 鼠标/接触点至 el 的水平距离
  private distX: number = 0

  // 鼠标/接触点至 el 的垂直距离
  private distY: number = 0

  // container 是否包含 el
  private get isContain() {
    return this.options.container.contains(this.el)
  }

  // el 位置信息
  private get elRect() {
    return this.el.getBoundingClientRect()
  }

  // container 位置信息
  private get containerRect() {
    return this.options.container.getBoundingClientRect()
  }

  // 初始化
  private init(el: HTMLElement, options: Partial<DragOptions>): void
  private init(options: Partial<DragOptions> & { el: HTMLElement }): void
  private init(el: any, options?: any) {
    if (!el) throw new Error(`[Error] arguments[0] is required an HTMLElement or Object with 'el' attribute that is an HTMLElement but got ${el}`)

    if (el instanceof HTMLElement) {
      this.el = el
    } else {
      if (!el.el) throw new Error(`[Error] arguments[0] is required an HTMLElement or Object with 'el' attribute that is an HTMLElement but got ${el.el}`)

      this.el = el.el

      options = el
    }

    this.options = this.getOptions(options)

    const { zIndex } = getComputedStyle(this.el)

    this.defaultZIndex = zIndex

    this.el.style.setProperty('cursor', 'move')
    this.el.style.setProperty('will-change', 'transform')

    // 将 el 位置转换为对应的 transform 值
    const { left, top } = this.getOffset()

    this.el.style.setProperty('transform', `translate3d(${left}px, ${top}px, 1px)`)
    this.el.style.setProperty('inset', '0')
    this.el.style.setProperty('margin', '0')

    this.el.addEventListener('mousedown', this.handleDragStart, { passive: false })
    this.el.addEventListener('touchstart', this.handleDragStart, { passive: false })
  }

  // 获取拖拽配置
  private getOptions(options: Partial<DragOptions>): DragOptions {
    return {
      container: options.container || this.defaultOptions.container,
      direction: options.direction,
      sticky: options.sticky,
      draggingClassName: options.draggingClassName || this.defaultOptions.draggingClassName
    }
  }

  // 拖拽区域边界值
  private getBoundary() {
    return {
      top: this.isContain ? 0 : this.containerRect.top,
      right: this.containerRect.right - this.elRect.width - (this.isContain ? this.containerRect.left : 0),
      bottom: this.containerRect.bottom - this.elRect.height - (this.isContain ? this.containerRect.top : 0),
      left: this.isContain ? 0 : this.containerRect.left
    }
  }

  // el 位置至 container 的距离 (> 0: 在 container 内; === 0: 贴边; < 0: 越界)
  private getOffset() {
    return {
      top: this.elRect.top - this.containerRect.top,
      right: this.containerRect.right - this.elRect.right,
      bottom: this.containerRect.bottom - this.elRect.bottom,
      left: this.elRect.left - this.containerRect.left
    }
  }

  // el transform
  private getMatrix3d() {
    const matrixReg = /^matrix\((?:[-\d.]+,\s*){4}([-\d.]+),\s*([-\d.]+)\)$/
    const matrix3dReg = /^matrix3d\((?:[-\d.]+,\s*){12}([-\d.]+),\s*([-\d.]+)(?:,\s*[-\d.]+){2}\)/

    const transform = getComputedStyle(this.el).transform
    const matrix3d = transform.match(matrix3dReg) || transform.match(matrixReg)

    const translateX = +matrix3d[1] // 元素至可视区域的水平距离
    const translateY = +matrix3d[2] // 元素至可视区域的垂直距离

    return {
      translateX,
      translateY
    }
  }

  // 拖拽开始
  private handleDragStart = (event: MouseEvent | TouchEvent) => {
    event.preventDefault()
    event.stopPropagation()

    this.el.classList.add(this.options.draggingClassName)

    const { translateX, translateY } = this.getMatrix3d()

    const clientX = event instanceof MouseEvent ? event.clientX : event.targetTouches[0].clientX
    const clientY = event instanceof MouseEvent ? event.clientY : event.targetTouches[0].clientY

    this.distX = clientX - translateX
    this.distY = clientY - translateY

    document.addEventListener('mousemove', this.handleDragging, { passive: false })
    document.addEventListener('touchmove', this.handleDragging, { passive: false })
    document.addEventListener('mouseup', this.handleDragEnd, { passive: false })
    document.addEventListener('touchend', this.handleDragEnd, { passive: false })

    // 拖拽过程中禁止响应其他事件
    this.el.style.setProperty('pointer-events', 'none')

    // 拖拽过程中为避免被其他元素影响（遮挡），应提高拖拽时的 el 层级
    this.el.style.setProperty('z-index', '9999')
  }

  // 拖拽过程
  private handleDragging = (event: MouseEvent | TouchEvent) => {
    event.preventDefault()
    event.stopPropagation()

    const clientX = event instanceof MouseEvent ? event.clientX : event.targetTouches[0].clientX
    const clientY = event instanceof MouseEvent ? event.clientY : event.targetTouches[0].clientY

    const moveX = clientX - this.distX
    const moveY = clientY - this.distY

    const { top, right, bottom, left } = this.getOffset()
    const { translateX, translateY } = this.getMatrix3d()

    let x = moveX
    let y = moveY

    // 处理拖拽方向
    switch (this.options.direction) {
      case 'horizontal':
        y = translateY
        break

      case 'vertical':
        x = translateX
        break
    }

    // 处理拖拽边界
    if (top <= 0) y = Math.max(moveY, translateY, this.getBoundary().top)
    if (right <= 0) x = Math.min(moveX, translateX, this.getBoundary().right)
    if (bottom <= 0) y = Math.min(moveY, translateY, this.getBoundary().bottom)
    if (left <= 0) x = Math.max(moveX, translateX, this.getBoundary().left)

    this.el.style.setProperty('transform', `translate3d(${x}px, ${y}px, 1px)`)
  }

  // 拖拽结束
  private handleDragEnd = (event: MouseEvent | TouchEvent) => {
    event.preventDefault()
    event.stopPropagation()

    document.removeEventListener('mousemove', this.handleDragging)
    document.removeEventListener('touchmove', this.handleDragging)

    this.el.classList.remove(this.options.draggingClassName)
    this.el.style.removeProperty('pointer-events')
    this.el.style.setProperty('z-index', `${this.defaultZIndex}`)

    if (this.options.sticky) {
      // 延迟 100ms，给人停顿效果
      setTimeout(() => {
        this.setSticky(this.options.sticky)
      }, 100)
    }
  }

  // 设置边沿吸附
  private setSticky(sticky: DragOptions['sticky']) {
    if (!sticky) return

    const duration = 0.25

    this.el.style.setProperty('transition', `transform ${duration}s ease-out`)

    const { translateX, translateY } = this.getMatrix3d()

    const { top, right, bottom, left } = this.getOffset()

    switch (sticky) {
      case 'top':
        this.el.style.setProperty('transform', `translate3d(${translateX}px, ${this.getBoundary().top}px, 1px)`)
        break

      case 'right':
        this.el.style.setProperty('transform', `translate3d(${this.getBoundary().right}px, ${translateY}px, 1px)`)
        break

      case 'bottom':
        this.el.style.setProperty('transform', `translate3d(${translateX}px, ${this.getBoundary().bottom}px,  1px)`)
        break

      case 'left':
        this.el.style.setProperty('transform', `translate3d(${this.getBoundary().left}px, ${translateY}px, 1px)`)
        break

      case 'horizontal':
        right < left ? this.setSticky('right') : this.setSticky('left')
        break

      case 'vertical':
        bottom < top ? this.setSticky('bottom') : this.setSticky('top')
        break

      case true:
        const min = Math.min(top, right, bottom, left)

        if (min === top) this.setSticky('top')
        if (min === right) this.setSticky('right')
        if (min === bottom) this.setSticky('bottom')
        if (min === left) this.setSticky('left')

        break
    }

    setTimeout(() => {
      this.el.style.removeProperty('transition')
    }, duration * 1000)
  }
}